import java.util.ArrayList;

/**
 * An output neuron class. An object of this class maintains its own set of
 * weights for each input. Internally it uses the sigmoid function and the delta
 * rule in order to train and classify test data. This class is specialized to
 * classify facial expressions from a 20x20 pixelmap.
 *
 */
public class Neuron {

    private final double LEARNING_RATE = 0.1;
    /*
     * The id of the neuron corresponds to the type of face it is meant to
     * recognize.
     */
    private final double id;

    private final ArrayList<ArrayList<Double>> weights;

    /**
     * Constructor
     * 
     * @param numInputs
     *            the amount of inputs to this neuron. MUST be an integer root.
     * @param id
     *            Used to identify this neuron.
     * @throws IllegalArgumentException
     *             if numInputs does not have an integer root.
     */
    public Neuron(final int numInputs, final double id)
            throws IllegalArgumentException {
        this.id = id;
        weights = new ArrayList<ArrayList<Double>>();
        final int size = (int) Math.sqrt(numInputs);
        // check that numInputs has an integer root.
        if (size != Math.ceil(Math.sqrt(numInputs))) {
            throw new IllegalArgumentException(
                    "Number of inputs must have an integer root.");
        }
        setupWeights(size, size);
    }

    /**
     * Sets up the initial weights for this neuron to all its inputs.
     * 
     * @param rows
     *            number of rows of the pixelmap grid
     * @param cols
     *            number of columns of the pixelmap grid
     */
    private void setupWeights(final int rows, final int cols) {

        for (int r = 0; r < rows; r++) {

            weights.add(new ArrayList<Double>(cols));
            for (int c = 0; c < cols; c++) {
                weights.get(r).add(1.6);
            }
        }
    }

    /**
     * The correct answer for a pixelmap is compared to the id of this neuron.
     * If they match we expect the sigmoid function to return a value of 1.
     * Otherwise we expect 0. Each training phase adjusts the weights towards
     * the expected values using the delta rule. After enough phases each neuron
     * should learn to recognize the patterns they are assigned.
     * 
     * @param data
     *            pixelmap training data to learn from.
     * @param answer
     *            the correct answer to the pixelmap.
     */
    public void train(final ArrayList<ArrayList<Double>> data,
            final Double answer) {

        final double sigM = sigmoid(calculateSum(data));
        double expectedValue = 1.0;
        /*
         * Each neuron is responsible for 1 facial expression. ID's and face
         * number match.
         */
        if (answer != id) {
            expectedValue = 0.0;
        }
        /*
         * we apply the delta rule to our weights to adjust towards our expected
         * value.
         */
        final Double deltaRuleFactor = LEARNING_RATE * (expectedValue - sigM);
        for (int i = 0; i < weights.size(); i++) {
            for (int j = 0; j < weights.size(); j++) {
                final Double dataVal = data.get(i).get(j);
                weights.get(i).set(j,
                        (dataVal * deltaRuleFactor) + weights.get(i).get(j));
            }
        }
    }

    private double sigmoid(final double x) {
        return (1 / (1 + Math.pow(Math.E, (-1 * x))));
    }

    /**
     * Classifies whether a pixelmap matches the learned pattern this neuron is
     * responsible for. The returned value corresponds to how much of the
     * pixelmap the neuron recognizes.
     * 
     * @param data
     *            pixelmap data to classify.
     * @return the certainty that the pixelmap belongs to this neuron.
     */
    public double classify(final ArrayList<ArrayList<Double>> data) {

        return sigmoid(calculateSum(data));

    }

    /**
     * Sums each input with their respective weight.
     * 
     * @param data
     *            the "inputs".
     * @return the sum
     */
    private Double calculateSum(final ArrayList<ArrayList<Double>> data) {
        Double sum = 0.0;
        for (int i = 0; i < data.size(); i++) {
            for (int j = 0; j < data.size(); j++) {
                sum += data.get(i).get(j) * weights.get(i).get(j);
            }
        }
        return sum;
    }

    /**
     * Gets the id of this neuron.
     * 
     * @return the id
     */
    public double getId() {
        return id;
    }
}
